﻿using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;

namespace GLDotNet
{
    public class Texture2D : GLObject
    {                   
        TextureFilter minFilter;
        TextureFilter magFilter;
        TextureWrapMode wrapS;
        TextureWrapMode wrapT;
                
        public TextureFilter MinFilter
        {
            get { return this.minFilter; }

            set
            {
                if (value != this.minFilter)
                {
                    this.minFilter = value;
                    this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
                    this.Context.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, this.minFilter);
                }
            }
        }

        public TextureFilter MagFilter
        {
            get { return this.magFilter; }

            set
            {
                if (value != this.magFilter)
                {
                    this.magFilter = value;
                    this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
                    this.Context.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, this.magFilter);
                }
            }
        }

        public TextureWrapMode WrapS
        {
            get { return this.wrapS; }

            set
            {
                if (value != this.wrapS)
                {
                    this.wrapS = value;
                    this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
                    this.Context.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, this.wrapS);
                }
            }
        }

        public TextureWrapMode WrapT
        {
            get { return this.wrapT; }

            set
            {
                if (value != this.wrapT)
                {
                    this.wrapT = value;
                    this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
                    this.Context.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, this.wrapT);
                }
            }
        }

        public int Width
        {
            get;
            private set;
        }

        public int Height
        {
            get;
            private set;
        }

        public Texture2D(GraphicsDevice graphicsDevice)
            : base(graphicsDevice)
        {
            uint texture = 0;
            this.Context.GenTextures(1, out texture);

            this.Handle = texture;

            this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
            this.Context.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, TextureFilter.Linear);
            this.Context.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, TextureFilter.Linear);

            this.minFilter = TextureFilter.Linear;
            this.magFilter = TextureFilter.Linear;
        }

        public static Texture2D FromFile(GraphicsDevice context, string fileName, Texture2DParams parameters)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            if (fileName == null)
                throw new ArgumentNullException("fileName");

            using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
                return FromStream(context, file, parameters);
        }

        public static Texture2D FromStream(GraphicsDevice context, Stream stream, Texture2DParams parameters)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            if (stream == null)
                throw new ArgumentNullException("stream");
                                                
            Texture2D texture = new Texture2D(context);
            context.BindTexture(TextureTarget.Texture2D, texture.Handle);

            Bitmap bitmap = (Bitmap)Bitmap.FromStream(stream);

            byte[] bytes = new byte[bitmap.Width * bitmap.Height * 4];

            BitmapData bitmapData = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            Marshal.Copy(bitmapData.Scan0, bytes, 0, bytes.Length);
            bitmap.UnlockBits(bitmapData);
            
            // Pixel format for little-endian machines is [B][G][R][A]. We need to convert to [R][G][B][A].
            // http://stackoverflow.com/questions/8104461/pixelformat-format32bppargb-seems-to-have-wrong-byte-order

            for (int i = 0; i < bytes.Length; i += 4)
            {
                byte b = bytes[i];
                bytes[i] = bytes[i + 2];
                bytes[i + 2] = b;

                if (parameters.ColorKey != null)
                {
                    uint pixel = GLUtility.MakePixelRGBA(bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]);

                    if (pixel == parameters.ColorKey.Value)
                        GLUtility.DecomposePixelRGBA(parameters.TransparentPixel, out bytes[i], out bytes[i + 1], out bytes[i + 2], out bytes[i + 3]);
                }
            }
            
            GCHandle bytesPtr = GCHandle.Alloc(bytes, GCHandleType.Pinned);

            try
            {
                texture.SetData(
                    0,
                    PixelFormat.Rgba,
                    bitmap.Width,
                    bitmap.Height,
                    0,
                    PixelFormat.Rgba,
                    PixelType.UnsignedByte,
                    bytesPtr.AddrOfPinnedObject());
            }
            finally
            {
                bytesPtr.Free();
            }
                                                
            return texture;
        }
        
        protected override void DeleteObject()
        {
            uint texture = this.Handle;
            this.Context.DeleteTextures(1, ref texture);
        }

        private void SetProperties(int width, int height)
        {
            this.Width = width;
            this.Height = height;
        }

        public void SetData<T>(int level, PixelFormat internalFormat, int width, int height, int border, PixelFormat format, PixelType pixelType, T[] pixels)
            where T : struct
        {
            this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
            this.Context.TexImage2D(TextureTarget.Texture2D, level, (int)internalFormat, width, height, border, format, pixelType, pixels);

            this.SetProperties(width, height);
        }

        public void SetData(int level, PixelFormat internalFormat, int width, int height, int border, PixelFormat format, PixelType pixelType, IntPtr pixels)
        {
            this.GraphicsDevice.BindTexture(TextureTarget.Texture2D, this.Handle);
            this.Context.TexImage2D(TextureTarget.Texture2D, level, (int)internalFormat, width, height, border, format, pixelType, pixels);

            this.SetProperties(width, height);
        }
    }
}
